#include <algorithm>
#include <chrono>
#include <ranges>

#include <gtest/gtest.h>

#include <Base/Definitions.h>

#include <EngineInterface/Description.h>
#include <EngineInterface/DescriptionEditService.h>
#include <EngineInterface/SimulationFacade.h>

#include "IntegrationTestFramework.h"

class DescriptionEditTests : public IntegrationTestFramework
{
public:
    DescriptionEditTests()
        : IntegrationTestFramework({100, 100})
    {}
    virtual ~DescriptionEditTests() = default;

protected:
    bool areAngelsCorrect(Description const& data) const
    {
        for (auto const& cell : data._cells) {
            if (!cell._connections.empty()) {
                float sumAngles = 0;
                for (auto const& connection : cell._connections) {
                    sumAngles += connection._angleFromPrevious;
                }
                if (std::abs(sumAngles - 360.0f) > NEAR_ZERO) {
                    return false;
                }
            }
        }
        return true;
    }
};


TEST_F(DescriptionEditTests, correctConnections)
{
    auto origData = DescriptionEditService::get().createRect(DescriptionEditService::CreateRectParameters().width(10).height(10).center({50.0f, 99.0f}));
    _simulationFacade->setSimulationData(origData);

    auto data = _simulationFacade->getSimulationData();

    DescriptionEditService::get().duplicate(data, {100, 100}, {100, 100});

    EXPECT_TRUE(areAngelsCorrect(data));
}


TEST_F(DescriptionEditTests, addThirdConnection1)
{
    auto data = Description().cells({
        CellDescription().id(1).pos({0, 0}),
        CellDescription().id(2).pos({1, 0}),
        CellDescription().id(3).pos({0, 1}),
        CellDescription().id(4).pos({0, -1}),
    });
    data.addConnection(1, 2);
    data.addConnection(1, 3);
    data.addConnection(1, 4);

    auto cell = data.getCellRef(1);

    EXPECT_EQ(3, cell._connections.size());

    auto connection1 = cell._connections.at(0);
    EXPECT_TRUE(approxCompare(1.0f, connection1._distance));
    EXPECT_TRUE(approxCompare(90.0f, connection1._angleFromPrevious));

    auto connection2 = cell._connections.at(1);
    EXPECT_TRUE(approxCompare(1.0f, connection2._distance));
    EXPECT_TRUE(approxCompare(90.0f, connection2._angleFromPrevious));

    auto connection3 = cell._connections.at(2);
    EXPECT_TRUE(approxCompare(1.0f, connection3._distance));
    EXPECT_TRUE(approxCompare(180.0f, connection3._angleFromPrevious));
}

TEST_F(DescriptionEditTests, addThirdConnection2)
{
    Description data;
    data._cells = {
        CellDescription().id(1).pos({0, 0}),
        CellDescription().id(2).pos({1, 0}),
        CellDescription().id(3).pos({-1, 0}),
        CellDescription().id(4).pos({0, 1}),
    };
    data.addConnection(1, 2);
    data.addConnection(1, 3);
    data.addConnection(1, 4);

    auto cell = data.getCellRef(1);

    EXPECT_EQ(3, cell._connections.size());

    auto connection1 = cell._connections.at(0);
    EXPECT_TRUE(approxCompare(1.0f, connection1._distance));
    EXPECT_TRUE(approxCompare(180.0f, connection1._angleFromPrevious));

    auto connection2 = cell._connections.at(1);
    EXPECT_TRUE(approxCompare(1.0f, connection2._distance));
    EXPECT_TRUE(approxCompare(90.0f, connection2._angleFromPrevious));

    auto connection3 = cell._connections.at(2);
    EXPECT_TRUE(approxCompare(1.0f, connection3._distance));
    EXPECT_TRUE(approxCompare(90.0f, connection3._angleFromPrevious));
}


enum class CellsOnCreature
{
    No,
    Yes
};
class DescriptionEditTests_CellIdGeneration
    : public DescriptionEditTests
    , public testing::WithParamInterface<CellsOnCreature>
{};

INSTANTIATE_TEST_SUITE_P(
    DescriptionEditTests_CellIdGeneration,
    DescriptionEditTests_CellIdGeneration,
    ::testing::Values(CellsOnCreature::No, CellsOnCreature::Yes));


TEST_P(DescriptionEditTests_CellIdGeneration, assignNewIds_differentCellIds)
{
    // Create test data
    Description data;
    if (GetParam() == CellsOnCreature::No) {
        data._cells = {CellDescription().id(0), CellDescription().id(1)};
    } else {
        data.addCreature(CreatureDescription().cells({CellDescription().id(0), CellDescription().id(1)}));
    }

    // Perform action
    data.assignNewIds();

    // Check result
    std::unordered_set<uint64_t> ids;
    data.forEachCell([&ids](auto const& cell) { ids.insert(cell._id); });

    EXPECT_EQ(2, ids.size());
}

TEST_P(DescriptionEditTests_CellIdGeneration, assignNewIds_sameCellIds)
{
    // Create test data
    auto createCollection = [] {
        if (GetParam() == CellsOnCreature::No) {
            return Description().cells({CellDescription().id(0), CellDescription().id(0)});
        } else {
            return Description().addCreature(CreatureDescription().cells({CellDescription().id(0), CellDescription().id(0)}));
        }
    };
    auto data = createCollection();

    // Perform action
    data.assignNewIds();

    // Check result
    std::unordered_set<uint64_t> ids;
    data.forEachCell([&ids](auto const& cell) { ids.insert(cell._id); });
    EXPECT_EQ(2, ids.size());
}

TEST_P(DescriptionEditTests_CellIdGeneration, assignNewIds_preserveOrder)
{
    // Create test data
    Description data;
    if (GetParam() == CellsOnCreature::No) {
        for (int i = 0; i < 10; ++i) {
            data._cells.emplace_back(CellDescription().id(i).age(i));
        }
        std::sort(data._cells.begin(), data._cells.end(), [](auto const& lhs, auto const& rhs) { return lhs._id > rhs._id; });
    } else {
        CreatureDescription creature;
        for (int i = 0; i < 10; ++i) {
            creature._cells.emplace_back(CellDescription().id(i).age(i));
        }
        std::sort(creature._cells.begin(), creature._cells.end(), [](auto const& lhs, auto const& rhs) { return lhs._id > rhs._id; });
        data.addCreature(creature);
    }

    // Perform action
    data.assignNewIds();

    // Check result
    auto const& cells = GetParam() == CellsOnCreature::No ? data._cells : data._creatures.front()._cells;
    std::map<int, CellDescription> ageToCell;
    for (auto const& cell : cells) {
        ageToCell.insert_or_assign(cell._age, cell);
    }
    EXPECT_EQ(10, ageToCell.size());

    std::optional<uint64_t> lastCellId;
    for (auto const& cell : ageToCell | std::views::values) {
        if (!lastCellId.has_value()) {
            lastCellId = cell._id;
        } else {
            EXPECT_TRUE(lastCellId.value() < cell._id);
        }
    }
}

TEST_F(DescriptionEditTests, assignNewIds_sameConnectionOnDifferentCreatures)
{
    // Create test data
    auto data = Description()
                    .cells({
                        CellDescription().id(0).connections({ConnectionDescription().cellId(1)}),
                        CellDescription().id(1).connections({ConnectionDescription().cellId(0)}),
                    })
                    .addCreature(CreatureDescription().cells({
                        CellDescription().id(0).connections({ConnectionDescription().cellId(1)}),
                        CellDescription().id(1).connections({ConnectionDescription().cellId(0)}),
                    }));

    // Perform action
    data.assignNewIds();

    // Check result
    std::unordered_set<uint64_t> ids;
    data.forEachCell([&ids](auto const& cell) { ids.insert(cell._id); });
    ASSERT_EQ(4, ids.size());

    ASSERT_EQ(2, data._cells.size());
    ASSERT_EQ(1, data._creatures.size());

    for (auto const& creature : data._creatures) {
        ASSERT_EQ(2, creature._cells.size());
        auto const& cell1 = creature._cells.front();
        auto const& cell2 = creature._cells.back();

        ASSERT_EQ(1, cell1._connections.size());
        EXPECT_EQ(cell2._id, cell1._connections.front()._cellId);

        ASSERT_EQ(1, cell2._connections.size());
        EXPECT_EQ(cell1._id, cell2._connections.front()._cellId);
    }
}

TEST_F(DescriptionEditTests, assignNewIds_connectionBetweenCreature)
{
    // Create test data
    auto data = Description()
                    .addCreature(CreatureDescription().cells({
                        CellDescription().id(0).connections({ConnectionDescription().cellId(2)}),
                        CellDescription().id(1),
                    }))
                    .addCreature(CreatureDescription().cells({
                        CellDescription().id(2).connections({ConnectionDescription().cellId(0)}),
                    }));

    // Perform action
    data.assignNewIds();

    // Check result
    std::unordered_set<uint64_t> ids;
    data.forEachCell([&ids](auto const& cell) { ids.insert(cell._id); });
    ASSERT_EQ(3, ids.size());

    ASSERT_EQ(0, data._cells.size());
    ASSERT_EQ(2, data._creatures.size());

    std::optional<CreatureDescription> smallCreature, largeCreature;
    for (auto const& creature : data._creatures) {
        if (creature._cells.size() == 1) {
            smallCreature = creature;
        }
        if (creature._cells.size() == 2) {
            largeCreature = creature;
        }
    }
    ASSERT_TRUE(smallCreature.has_value());
    ASSERT_TRUE(largeCreature.has_value());

    EXPECT_EQ(1, smallCreature->_cells.front()._connections.size());
    auto connectedCellId = smallCreature->_cells.front()._connections.front()._cellId;

    std::optional<CellDescription> cellWithoutConnection, cellWithConnection;
    for (auto const& cell : largeCreature->_cells) {
        if (cell._connections.empty()) {
            cellWithoutConnection = cell;
        } else {
            cellWithConnection = cell;
        }
    }
    ASSERT_TRUE(cellWithoutConnection.has_value());
    ASSERT_TRUE(cellWithConnection.has_value());

    ASSERT_EQ(1, cellWithConnection->_connections.size());
    EXPECT_EQ(connectedCellId, cellWithConnection->_id);
    EXPECT_EQ(smallCreature->_cells.front()._id, cellWithConnection->_connections.front()._cellId);
}

TEST_F(DescriptionEditTests, assignNewIds_connectionNotContained)
{
    // Create test data
    auto data = Description()
                    .cells({
                        CellDescription().id(0).connections({ConnectionDescription().cellId(3)}),
                        CellDescription().id(1),
                    })
                    .addCreature(CreatureDescription().cells({
                        CellDescription().id(2).connections({ConnectionDescription().cellId(4)}),
                    }));
    // Perform action
    data.assignNewIds();

    // Check result
    std::unordered_set<uint64_t> ids;
    data.forEachCell([&ids](auto const& cell) { ids.insert(cell._id); });
    ASSERT_EQ(3, ids.size());

    ASSERT_EQ(2, data._cells.size());
    ASSERT_EQ(1, data._creatures.size());

    std::optional<CellDescription> cellWithoutConnection, cellWithConnection;
    for (auto const& cell : data._cells) {
        if (cell._connections.empty()) {
            cellWithoutConnection = cell;
        } else {
            cellWithConnection = cell;
        }
    }
    ASSERT_TRUE(cellWithoutConnection.has_value());
    ASSERT_TRUE(cellWithConnection.has_value());

    ASSERT_EQ(1, cellWithConnection->_connections.size());
    EXPECT_EQ(3, cellWithConnection->_connections.front()._cellId);

    auto const& creature = data._creatures.front();
    ASSERT_EQ(1, creature._cells.size());

    ASSERT_EQ(1, creature._cells.front()._connections.size());
    ASSERT_EQ(4, creature._cells.front()._connections.front()._cellId);
}

TEST_F(DescriptionEditTests, assignNewIds_cellWithLastConstructedCellId_contained)
{
    // Create test data
    auto data = Description().cells({
        CellDescription().id(0).cellType(ConstructorDescription().lastConstructedCellId(1)),
        CellDescription().id(1),
    });

    // Perform action
    data.assignNewIds();

    // Check result
    std::unordered_set<uint64_t> ids;
    data.forEachCell([&ids](auto const& cell) { ids.insert(cell._id); });
    ASSERT_EQ(2, ids.size());

    ASSERT_EQ(2, data._cells.size());
    ASSERT_EQ(0, data._creatures.size());

    std::optional<CellDescription> constructor, base;
    for (auto const& cell : data._cells) {
        if (cell.getCellType() == CellType_Constructor) {
            constructor = cell;
        }
        if (cell.getCellType() == CellType_Base) {
            base = cell;
        }
    }
    ASSERT_TRUE(constructor.has_value());
    ASSERT_TRUE(base.has_value());

    EXPECT_EQ(base->_id, std::get<ConstructorDescription>(constructor->_cellType)._lastConstructedCellId);
}

TEST_F(DescriptionEditTests, assignNewIds_cellWithLastConstructedCellId_notContained)
{
    // Create test data
    auto data = Description().cells({
        CellDescription().id(0).cellType(ConstructorDescription().lastConstructedCellId(2)),
        CellDescription().id(1),
    });

    // Perform action
    data.assignNewIds();

    // Check result
    std::unordered_set<uint64_t> ids;
    data.forEachCell([&ids](auto const& cell) { ids.insert(cell._id); });
    ASSERT_EQ(2, ids.size());

    ASSERT_EQ(2, data._cells.size());
    ASSERT_EQ(0, data._creatures.size());

    std::optional<CellDescription> constructor, base;
    for (auto const& cell : data._cells) {
        if (cell.getCellType() == CellType_Constructor) {
            constructor = cell;
        }
        if (cell.getCellType() == CellType_Base) {
            base = cell;
        }
    }
    ASSERT_TRUE(constructor.has_value());
    ASSERT_TRUE(base.has_value());

    EXPECT_EQ(2, std::get<ConstructorDescription>(constructor->_cellType)._lastConstructedCellId);
}

TEST_F(DescriptionEditTests, assignNewIds_differentParticleIds)
{
    // Create test data
    auto data = Description().particles({ParticleDescription().id(0), ParticleDescription().id(1)});

    // Perform action
    data.assignNewIds();

    // Check result
    std::unordered_set<uint64_t> ids;
    for (auto const& cell : data._particles) {
        ids.insert(cell._id);
    }
    EXPECT_EQ(2, ids.size());
}

TEST_F(DescriptionEditTests, assignNewIds_sameParticleIds)
{
    // Create test data
    auto data = Description().particles({ParticleDescription().id(0), ParticleDescription().id(0)});

    // Perform action
    data.assignNewIds();

    // Check result
    std::unordered_set<uint64_t> ids;
    for (auto const& cell : data._particles) {
        ids.insert(cell._id);
    }
    EXPECT_EQ(2, ids.size());
}

TEST_F(DescriptionEditTests, assignNewIds_differentCreatureIds)
{
    // Create test data
    auto data = Description()
                    .addCreature(CreatureDescription().id(0).cells({CellDescription().id(0), CellDescription().id(0)}))
                    .addCreature(CreatureDescription().id(1).cells({CellDescription().id(0), CellDescription().id(0)}));

    // Perform action
    data.assignNewIds();

    // Check result
    {
        std::unordered_set<uint64_t> ids;
        for (auto const& creature : data._creatures) {
            ids.insert(creature._id);
        }
        EXPECT_EQ(2, ids.size());
    }
    {
        std::unordered_set<uint64_t> ids;
        data.forEachCell([&ids](auto const& cell) { ids.insert(cell._id); });
        ASSERT_EQ(4, ids.size());
    }
}

TEST_F(DescriptionEditTests, assignNewIds_sameCreatureIds)
{
    // Create test data
    auto data = Description()
                    .addCreature(CreatureDescription().id(0).cells({CellDescription().id(0), CellDescription().id(0)}))
                    .addCreature(CreatureDescription().id(0).cells({CellDescription().id(0), CellDescription().id(0)}));

    // Perform action
    data.assignNewIds();

    // Check result
    {
        std::unordered_set<uint64_t> ids;
        for (auto const& creature : data._creatures) {
            ids.insert(creature._id);
        }
        EXPECT_EQ(2, ids.size());
    }
    {
        std::unordered_set<uint64_t> ids;
        data.forEachCell([&ids](auto const& cell) { ids.insert(cell._id); });
        ASSERT_EQ(4, ids.size());
    }
}

TEST_F(DescriptionEditTests, assignNewIds_creatureWithAncestorId_contained)
{
    // Create test data
    auto data = Description()
                    .addCreature(CreatureDescription().id(2).cells({CellDescription()}))
                    .addCreature(CreatureDescription().id(3).ancestorId(2).cells({CellDescription()}));

    // Perform action
    data.assignNewIds();

    // Check result
    std::unordered_set<uint64_t> ids;
    for (auto const& creature : data._creatures) {
        ids.insert(creature._id);
    }
    EXPECT_EQ(2, ids.size());

    std::optional<CreatureDescription> offspring, ancestor;
    for (auto const& creature : data._creatures) {
        if (creature._ancestorId.has_value()) {
            offspring = creature;
        } else {
            ancestor = creature;
        }
    }
    ASSERT_TRUE(offspring.has_value());
    ASSERT_TRUE(ancestor.has_value());

    EXPECT_EQ(ancestor->_id, offspring->_ancestorId.value());
}

TEST_F(DescriptionEditTests, assignNewIds_creatureWithAncestorId_notContained)
{
    // Create test data
    auto data = Description()
                    .addCreature(CreatureDescription().id(2).cells({CellDescription()}))
                    .addCreature(CreatureDescription().id(3).ancestorId(1).cells({CellDescription()}));

    // Perform action
    data.assignNewIds();

    // Check result
    std::unordered_set<uint64_t> ids;
    for (auto const& creature : data._creatures) {
        ids.insert(creature._id);
    }
    EXPECT_EQ(2, ids.size());

    std::optional<CreatureDescription> offspring, ancestor;
    for (auto const& creature : data._creatures) {
        if (creature._ancestorId.has_value()) {
            offspring = creature;
        } else {
            ancestor = creature;
        }
    }
    ASSERT_TRUE(offspring.has_value());
    ASSERT_TRUE(ancestor.has_value());

    EXPECT_EQ(1, offspring->_ancestorId.value());
}

TEST_F(DescriptionEditTests, assignNewIds_creatureWithAncestorId_notUnique)
{
    // Create test data
    auto data = Description()
                    .addCreature(CreatureDescription().id(2).cells({CellDescription()}))
                    .addCreature(CreatureDescription().id(2).cells({CellDescription()}))
                    .addCreature(CreatureDescription().id(3).ancestorId(2).cells({CellDescription()}));

    // Perform action
    data.assignNewIds();

    // Check result
    std::unordered_set<uint64_t> ids;
    for (auto const& creature : data._creatures) {
        ids.insert(creature._id);
    }
    EXPECT_EQ(3, ids.size());

    for (auto const& creature : data._creatures) {
        EXPECT_TRUE(!creature._ancestorId.has_value());
    }
}

TEST_F(DescriptionEditTests, adaptMaxIds)
{
    auto data = Description()
                    .creatures({
                        CreatureDescription().id(3).cells({CellDescription().id(5)}),
                        CreatureDescription().cells({CellDescription()}),
                    })
                    .particles({
                        ParticleDescription().id(7),
                        ParticleDescription(),
                    });

    EXPECT_LT(data._creatures.at(0)._id, data._creatures.at(1)._id);
    EXPECT_LT(data._creatures.at(0)._cells.at(0)._id, data._creatures.at(1)._cells.at(0)._id);
    EXPECT_LT(data._particles.at(0)._id, data._particles.at(1)._id);
}


TEST_F(DescriptionEditTests, flattenTopology_longDiagonalCreature_lowerRight)
{
    auto const& WorldWidth = 346;
    auto const& WorldHeight = 100;

    CreatureDescription creature;
    for (int i = 0; i < 1000; ++i) {
        creature._cells.emplace_back(CellDescription().id(i).pos({toFloat((50 + i) % WorldWidth), toFloat((50 + i) % WorldHeight)}));
    }
    auto data = Description().creatures({creature});
    for (int i = 1; i < 1000; ++i) {
        data.addConnection(i - 1, i);
    }

    DescriptionEditService::get().flattenTopology(data, IntVector2D{WorldWidth, WorldHeight});

    ASSERT_EQ(1, data._creatures.size());

    auto creatureAfter = data._creatures.front();
    ASSERT_EQ(1000, creatureAfter._cells.size());

    for (int i = 0; i < 1000; ++i) {
        auto const& refCell = data.getCellRef(0);
        auto const& cell = data.getCellRef(i);
        EXPECT_TRUE(approxCompare(toFloat(i), cell._pos.x - refCell._pos.x));
        EXPECT_TRUE(approxCompare(toFloat(i), cell._pos.y - refCell._pos.y));
    }
}

TEST_F(DescriptionEditTests, flattenTopology_longDiagonalCreature_upperLeft)
{
    auto const& WorldWidth = 346;
    auto const& WorldHeight = 100;

    CreatureDescription creature;
    for (int i = 0; i < 1000; ++i) {
        creature._cells.emplace_back(CellDescription().id(i).pos({toFloat((50 - i + WorldWidth) % WorldWidth), toFloat((50 - i + WorldHeight) % WorldHeight)}));
    }
    auto data = Description().creatures({creature});
    for (int i = 1; i < 1000; ++i) {
        data.addConnection(i - 1, i);
    }

    DescriptionEditService::get().flattenTopology(data, IntVector2D{WorldWidth, WorldHeight});

    ASSERT_EQ(1, data._creatures.size());

    auto creatureAfter = data._creatures.front();
    ASSERT_EQ(1000, creatureAfter._cells.size());

    for (int i = 0; i < 1000; ++i) {
        auto const& refCell = data.getCellRef(0);
        auto const& cell = data.getCellRef(i);
        EXPECT_TRUE(approxCompare(toFloat(i), refCell._pos.x - cell._pos.x));
        EXPECT_TRUE(approxCompare(toFloat(i), refCell._pos.y - cell._pos.y));
    }
}
